package idv.takeshi.software.productline.bookstore.domain.model.order;

import idv.takeshi.software.productline.bookstore.domain.model.book.Book;
import idv.takeshi.software.productline.bookstore.domain.model.deliveryservice.DeliveryService;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.Table;

import org.apache.commons.lang.Validate;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A model class represents as a Order.
 * @author takeshi
 *
 */
@Entity
@Table(name = "CUSTOMER_ORDER")
@NamedQueries(
		{
			@NamedQuery(name = "findAllOrders", query = "SELECT o FROM Order o ORDER BY o.updateTime DESC")}
	)
public class Order {
	
	private static final Log log = LogFactory.getLog(Order.class);
	
	@Id
    @GeneratedValue(strategy = GenerationType.TABLE)
	private Long id;
	
	@Column(name = "RECIPIENT_ADDRESS", length = 100, nullable = false)
	private String recipientAddress;
	
	@Column(name = "RECIPIENT_NAME", length = 40, nullable = false)
	private String recipientName;
	
	@Column(name = "RECIPIENT_CELL_PHONE_NO", length = 20, nullable = false)
	private String recipientCellPhoneNumber;
	
	@Column(name = "EXTRA_REQUIREMENTS", length = 100)
	private String extraRequirements;
	
	@Column(nullable = false, length = 50)
	@Enumerated(EnumType.STRING)
	private OrderStatus status;
	
	@Column(nullable = false)
	private BigDecimal price;
	
	@Column(name = "DELIVERY_FEE", nullable = false)
	private BigDecimal deliveryFee;
	
	@Column(nullable = false, name = "CREATE_TIME")
	private Date createTime;
	
	@Column(nullable = false, name = "UPDATE_TIME")
	private Date updateTime;
	
	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "CUSTOMER_ORDER_ID", nullable = false)
	@OrderBy("sequence ASC")
	private Set<OrderItem> orderItems = new LinkedHashSet<OrderItem>();
	
	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "CUSTOMER_ORDER_ID")
	@OrderBy("sequence ASC")
	private Set<PickingList> pickingLists = new LinkedHashSet<PickingList>();
	 
	@OneToOne(fetch=FetchType.EAGER, cascade = CascadeType.ALL)
	private Payment payment;
	
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "DELIVERY_SERVICE_ID")
	private DeliveryService deliveryService;
	
	@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
	private Invoice invoice;
	 
	/**
	 * Check all orderItems status whether need replenishing.
	 */
	public void checkOrderItemsStatus() {
		this.status.checkOrderItemsStatus(this);
	}
	 
	/**
	 * Set this order status to underProcessing.
	 * @throws OrderException 
	 */
	public void process() throws OrderException {
		this.status.process(this);
	}
	 
	/**
	 * Set this order status to underDelivering.
	 */
	public void deliver() {
		this.status.deliver(this);
	}
	 
	/**
	 * Set this order status to inConvenientStore or received.  
	 */
	public void arrive() {
		this.status.arrive(this);
	}
	 
	/**
	 * Set this orde status to close.
	 */
	public void close() {
		this.status.close(this);
	}
	 
	/**
	 * Set this order status to cancel. 
	 */
	public void cancel() {
		this.status.cancel(this);
	}
	 
	/**
	 * Add a new pickingList for this order.
	 * @return
	 */
	public PickingList addPickingList() {
		Date dateTime = null;
		PickingList pickingList = new PickingList();
		pickingList.setSequence(this.getPickingListSequence());
		pickingList.setStatus(PickingListStatus.UNDER_PROCESSING);
		dateTime = new Date();
		pickingList.setCreateTime(dateTime);
		pickingList.setUpdateTime(dateTime);
		
		for(OrderItem orderItem : this.orderItems){
			if(orderItem.getStatus().equals(OrderItemStatus.OK))
				pickingList.addPickingListItem(orderItem);
		}
		this.pickingLists.add(pickingList);
		return pickingList;
	}
	
	/**
	 * Get a active PickingList for this order. 
	 * @return
	 */
	public PickingList getActivePickingList(){
		PickingList pickingList = null;
		PickingList tmpPickingList = null;
		for(Iterator<PickingList> it = this.pickingLists.iterator(); it.hasNext();){
			tmpPickingList = it.next();
			if(tmpPickingList.getStatus() != PickingListStatus.CLOSE && 
					tmpPickingList.getStatus() != PickingListStatus.CANCEL){
				pickingList = tmpPickingList;
				break;
			}
		}
		return pickingList;
	}
	 
	/**
	 * @return the pickingLists
	 */
	public Set<PickingList> getPickingLists() {
		return pickingLists;
	}
	
	/**
	 * Get pickingList by sequence.
	 * @param sequence
	 * @return
	 */
	public PickingList getPickingList(int sequence){
		Validate.isTrue(sequence > 0, "sequence:" + sequence + " shall always greater than 0");
		PickingList pickingList = null;
		for(PickingList tmpPickingList : this.pickingLists){
			if(tmpPickingList.getSequence() == sequence){
				pickingList = tmpPickingList;
				break;
			}
		}
		return pickingList;
	}

	/**
	 * Get order price.
	 * 
	 * @return
	 */
	public BigDecimal getPrice() {
		if(null == this.price){
			this.calculatePrice();
		}
		return price;
	}
	
	/**
	 * Calculate the price of this order.
	 */
	public void calculatePrice(){
		price = new BigDecimal("0");
		
		for(OrderItem orderItem : this.orderItems){
			price = price.add(orderItem.getTotalUnitPrice());
		}
		
		this.deliveryFee = new BigDecimal("0");
		if(!this.deliveryService.isOverDeliveryFeeOffThreshold(price)){
			this.deliveryFee = this.deliveryService.getDeliveryFee();
		}
		
		price = this.deliveryService.addDeliveryFee(price);
		this.setUpdateTime(new Date());
	}
	 
	/**
	 * @return the deliveryFee
	 */
	public BigDecimal getDeliveryFee() {
		return deliveryFee;
	}

	/**
	 * Add a book and corresponding quantity into this order as an orderItem.
	 * @param book
	 * @param quantity
	 * @return 
	 */
	public OrderItem addOrderItem(Book book, int quantity) {
		Validate.notNull(book, "book shall always not be null");
		Validate.isTrue(quantity > 0, "quantity:" + quantity + " shall greater than 0");
		OrderItem orderItem = new OrderItem();
		
		orderItem.setSequence(getOrderItemSequence());
		orderItem.setStatus(OrderItemStatus.OK);
		orderItem.setQuantity(quantity);
		orderItem.setUpdateTime(new Date());
		orderItem.setOrderedBook(book);
		orderItem.setUnitPrice(book.getUnitPrice());
		
		this.orderItems.add(orderItem);
		return orderItem;
	}
	
	/**
	 * Add a invoice for this order.
	 * @param invoiceClass
	 * @param invoiceTitle
	 * @param uniformNumber
	 * @return
	 */
	public <T extends Invoice> Invoice addInvoice(Class<T> invoiceClass, String invoiceTitle, 
			String uniformNumber) {
		Validate.notNull(invoiceClass, "invoiceClass shall always not be null");
		Invoice invoice = null;
		
		try {
			
			invoice = invoiceClass.newInstance();
			
			invoice.setInvoiceTitle(invoiceTitle);
			invoice.setUniformNumber(uniformNumber);
			invoice.setUpdateTime(new Date());
			this.invoice = invoice;
		} catch (Exception e) {
			String msg = "initiate Invoice instance for class:" + invoiceClass + " failed";
			log.error(msg, e);
			throw new RuntimeException(msg, e);
		}
		return this.invoice;
	}
	 
	/**
	 * Get invoice for this order.
	 * @return
	 */
	public Invoice getInvoice() {
		return this.invoice;
	}
	 
	/**
	 * Add a payment for this Order.
	 * @param <T>
	 * @param paymentClass
	 * @return
	 */
	public <T extends Payment> Payment addPayment(Class<T> paymentClass) {
		Validate.notNull(paymentClass, "paymentClass shall always not be null");
		Payment payment = null;
		try {
			
			payment = paymentClass.newInstance();
			this.calculatePrice();
			payment.setAmount(this.getPrice());
			payment.setUpdateTime(new Date());
			this.payment = payment;
		} catch (Exception e) {
			String msg = "initial payment instance for class:" + paymentClass + " failed";
			log.error(msg, e);
			throw new RuntimeException(msg, e);
		}
		
		return this.payment;
	}
	 
	/**
	 * Get payment.
	 * @return
	 */
	public Payment getPayment() {
		return this.payment;
	}
	 
	/**
	 * @return the deliveryService
	 */
	public DeliveryService getDeliveryService() {
		return deliveryService;
	}

	/**
	 * @param deliveryService the deliveryService to set
	 */
	public void setDeliveryService(DeliveryService deliveryService) {
		this.deliveryService = deliveryService;
	}
	 
	/**
	 * @return the status
	 */
	public OrderStatus getStatus() {
		return status;
	}

	/**
	 * Get unmodifiable orderItems.
	 * @return
	 */
	public Set<OrderItem> getOrderItems() {
		return Collections.unmodifiableSet(orderItems);
	}

	/**
	 * @return the id
	 */
	public Long getId() {
		return id;
	}

	/**
	 * @param id the id to set
	 */
	public void setId(Long id) {
		this.id = id;
	}

	/**
	 * @return the recipientAddress
	 */
	public String getRecipientAddress() {
		return recipientAddress;
	}

	/**
	 * @param recipientAddress the recipientAddress to set
	 */
	public void setRecipientAddress(String recipientAddress) {
		this.recipientAddress = recipientAddress;
	}

	/**
	 * @return the recipientName
	 */
	public String getRecipientName() {
		return recipientName;
	}

	/**
	 * @param recipientName the recipientName to set
	 */
	public void setRecipientName(String recipientName) {
		this.recipientName = recipientName;
	}

	/**
	 * @return the recipientCellPhoneNumber
	 */
	public String getRecipientCellPhoneNumber() {
		return recipientCellPhoneNumber;
	}

	/**
	 * @param recipientCellPhoneNumber the recipientCellPhoneNumber to set
	 */
	public void setRecipientCellPhoneNumber(String recipientCellPhoneNumber) {
		this.recipientCellPhoneNumber = recipientCellPhoneNumber;
	}

	/**
	 * @return the extraRequirements
	 */
	public String getExtraRequirements() {
		return extraRequirements;
	}

	/**
	 * @param extraRequirements the extraRequirements to set
	 */
	public void setExtraRequirements(String extraRequirements) {
		this.extraRequirements = extraRequirements;
	}

	/**
	 * Set createTime.
	 * @param createTime
	 */
	public void setCreateTime(Date createTime) {
		Validate.notNull(createTime, "createTime shall always not be null");
		this.createTime = createTime;
	}

	/**
	 * @return the createTime
	 */
	public Date getCreateTime() {
		return createTime;
	}

	/**
	 * Set updateTime.
	 * @param updateTime
	 */
	public void setUpdateTime(Date updateTime) {
		Validate.notNull(updateTime, "updateTime shall always not be null");
		this.updateTime = updateTime;
	}

	/**
	 * @return the updateTime
	 */
	public Date getUpdateTime() {
		return updateTime;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == null) { return false; }
		   if (obj == this) { return true; }
		   if (!(obj instanceof Order)) {
		     return false;
		   }
		   Order rhs = (Order) obj;
		   return new EqualsBuilder()
			   .append(this.id, rhs.id)
			   .append(this.recipientName, rhs.recipientName)
			   .append(this.recipientAddress, rhs.recipientAddress)
			   .append(this.recipientCellPhoneNumber, rhs.recipientCellPhoneNumber)
			   .append(this.extraRequirements, rhs.extraRequirements)
			   .append(this.status, rhs.status)
			   .append(this.price, rhs.price)
			   .append(this.deliveryFee, rhs.deliveryFee)
			   .append(this.createTime, rhs.createTime)
			   .append(this.updateTime, rhs.updateTime)
			   .append(this.orderItems, rhs.orderItems)
			   .append(this.pickingLists, rhs.pickingLists)
			   .append(this.payment, rhs.payment)
			   .append(this.deliveryService, rhs.deliveryService)
			   .append(this.invoice, rhs.invoice)
		       .isEquals();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return new HashCodeBuilder(41, 43).
	       append(this.id).
	       append(this.recipientName).
	       append(this.recipientAddress).
	       append(this.recipientCellPhoneNumber).
	       append(this.extraRequirements).
	       append(this.status).
	       append(this.price).
	       append(this.deliveryFee).
	       append(this.createTime).
	       append(this.updateTime).
	       append(this.orderItems).
	       append(this.pickingLists).
	       append(this.payment).
	       append(this.deliveryService).
	       append(this.invoice).
	       toHashCode();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return ReflectionToStringBuilder.toString(this, ToStringStyle.MULTI_LINE_STYLE);
	}

	/**
	 * @param status the status to set
	 */
	protected void setStatus(OrderStatus status) {
		this.status = status;
	}

	private int getPickingListSequence(){
		int sequence = 1;
		if(pickingLists.size() > 0){
			sequence = pickingLists.size() + 1;
		}
		return sequence;
	}

	private int getOrderItemSequence(){
		int sequence = 1;
		if(orderItems.size() > 0){
			sequence = orderItems.size() + 1;
		}
		return sequence;
	}
	
}
 
